#!/usr/bin/env python
#
# Copyright 2009 Benjamin Davis
#
# This file is part of resistencia.
#
# resistencia is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# resistencia is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with resistencia.  If not, see <http://www.gnu.org/licenses/>.

import socket
import sys
import threading
import time

from players import *
from dungeons import *
from commands import *

class Clients(list):
    """Builds on the list class to provide functions that act on all clients"""
    def close(self):
        """Closes the connection of all clients"""
        for client in self:
            client.connection.close()
    
    def send(self, data):
        """Sends data to all clients"""
        for client in self:
            client.send(data)

class Client(threading.Thread):
    """Created for each client which connects"""
    def __init__(self, dungeon, (conn, addr)):
        print addr, "connected."
        threading.Thread.__init__(self)
        # Add the register and login commands
        self.commands = [Login('login'), Register('register')]
        self.connection = conn
        self.address = addr
        self.stopEvent = threading.Event()
        self.connection.settimeout(0.1)
        self.dungeon = dungeon
        self.player = None
        self.send('Welcome to resistencia,', self.address[0] + '.')
        self.send()
        self.send('If you a new user, please enter "register <username>\
 <password>" to create a new character. Otherwise, please enter "login\
 <username> <password>" to resume a previous session.')
        self.send('Neither usernames nor passwords should contain spaces.')
        self.send()
    
    def run(self):
        # Begin login/register code
        # Whilst we haven't been told to stop and the client hasn't logged in
        while (not self.stopEvent.isSet()) and self.player is None:
            try:
                # Recieve data from the client
                data = self.connection.recv(1024)
                # If client disconnects
                if not data:
                    # Remove from the list
                    clients.remove(self)
                    # Stop the thread
                    self.stopEvent.set()
                else:
                    # Act on the client input
                    self.do(data)
            # Will timeout every 100ms, this is to check whether we need to stop
            # the thread or not.
            except socket.timeout:
                pass
            except socket.error:
                clients.remove(self)
                self.stopEvent.set()
                raise
        
        # Begin play code
        # Whilst we haven't been told to stop
        while not self.stopEvent.isSet():
            try:
                # Recieve data from the client
                data = self.connection.recv(1024)
                # If client disconnects
                if not data:
                    # Remove from the list
                    clients.remove(self)
                    # Broadcast to the player's room that they left
                    for someone in self.player.room.players:
                        if someone != self.player:
                            someone.client.send(self.player.name + " leaves to the void.")
                    # Remove the player from the room
                    self.player.room.players.remove(self.player)
                    # Remove the client from the player
                    # The login code checks whether the player already has a
                    # client assigned to it, and if so, denies the login, so we
                    # need to make sure that this is cleanly done.
                    self.player.client = None
                    # Print debugging info
                    print self.address, "disconnected."
                    # Stop the thread
                    self.stopEvent.set()
                else:
                    # Act on the data
                    self.player.do(data)
            except socket.timeout:
                pass
            # on an error
            except socket.error:
                # Act as though the player had wilfully left
                clients.remove(self)
                for someone in self.player.room.players:
                    if someone != self.player:
                        someone.client.send(self.player.name + " leaves to the void.")
                self.player.room.players.remove(self.player)
                self.player.client = None
                print self.address, "disconnected."
                self.stopEvent.set()
                raise

    def join(self):
        """Ends the session for client"""
        # Stop the thread
        self.stopEvent.set()
        # Close the socket
        self.connection.close()
        self.player.client = None
        print self.address, "disconnected."
        threading.Thread.join(self)
        
    def send(self, *args):
        """Concatenates all arguments space-seperated and sends to the client
        suffixed by a new-line.
        
        If you wish to send data to the client without a new-line please use
        client.connection.send()"""
        data = ""
        for arg in args:
            data += ' ' + str(arg)
        self.connection.send(data.strip() + '\n')
        
    def do(self, data):
        """Parses login data"""
        commands = []
        # For each command
        for command in self.commands:
            # If the data starts with a command name
            if data.startswith(command):
                # Then add command to the list
                commands.append(command)
        if len(commands) > 0:
            commands.sort()
            commands.reverse()
            # Automatically take the longest matching command
            commands[0].run(self, data[len(commands[0]):].strip()) 
        else:
            self.send("Couldn't find command.")
        self.send()

class Login(Command):
    """The 'login' command. Assigns an existing player to the client upon
    successful entry of a player name and password"""
    def run(self, client, data):
        # Split the data into individual words
        data = data.split()
        # If client entered the wrong amount of parameters, give syntax
        if len(data) != 2:
            client.send('Please enter: login <username> <password>')
            client.send('where username is a valid username. To register\
 please use: register <username> <password>')
            client.send('Neither usernames nor passwords should contain spaces.\
')
        # If they entered the right number of parameters, check for validity
        else:
            # Capitalise the first letter of the username
            data[0] = data[0].capitalize()
            player = client.dungeon.players.by_name(data[0])
            # If player exists
            if player:
                # If someone is already logged into player
                if player.client is not None:
                    client.send('Player', data[0], 'is currently being used.')
                else:
                    # If password is correct
                    if player.password == data[1]:
                        # Set client's player to player
                        client.player = player
                        player.client = client  
                        client.player.enter(client.player.room, "from the void")
                    else:
                        client.send('Incorrect password')
            else:
                client.send("Player", data[0], "doesn't exist.")

class Register(Command):
    """The 'register' command. Assigns a new player to the client"""
    def run(self, client, data):
        # Split the data into individual words
        data = data.split()
        # If client entered the wrong amount of parameters, give syntax
        if len(data) != 2:
            client.send('Please enter: register <username> <password>')
            client.send('To login please use: login <username> <password>')
            client.send('Neither usernames nor passwords should contain spaces.\
')
        else:
            data[0] = data[0].capitalize()
            # If player exists:
            if client.dungeon.players.by_name(data[0]):
                client.send('Player', data[0], 'already exists.')
                client.send('To login please use: login <username> <password>')
            else:
                # Capitalise the first letter of the username
                # Create a new player
                player = Player(client, data[0], data[1])
                # Add to the dungeon's list of players
                client.dungeon.players.append(player)
                # Set the client's active player
                client.player = player
                client.send('Player', data[0], 'successfully created.')
                client.send()
                client.player.enter(client.dungeon.get_room_at_xy(2, 1), "from the void")

def main(port, dungeon):
    """The main game loop"""
    # Assign a socket to listen for new connections
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Bind to the specified port
    listener.bind(('', port))
    listener.listen(1)
    
    # Set up a list of all clients
    global clients
    clients = Clients()

    try:
        while True:
            # Wait for a new client to connect and initialise a new
            # Client object for them
            clients.append(Client(dungeon, listener.accept()))
            # Start the thread
            clients[-1].start()
    # On error try to exit cleanly
    except:
        # Close all client sockets
        for client in clients:
            client.join()
        # Close listener socket
        listener.close()
        raise
